package lspay

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	uuid "gopkg.in/satori/go.uuid.v1"

	"codeup.aliyun.com/5f9118049cffa29cfdd3be1c/util/cipher/payserver"
)

type method string

const (
	MethodPay    method = "/api/v1/pay/all"
	MethodRefund method = "/api/v1/pay/refund"
	MethodClose  method = "/api/v1/pay/close"
	MethodQuery  method = "/api/v1/merchant/order/detail"
)

type (
	IPay interface {
		WithHeaders(headers map[string]string) IPay
		All(ctx context.Context, req *AllReq, m method) (*AllResp, error)

		Pay(ctx context.Context, req *PayRequest) (*PayResponse, error)
		Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error)
		Close(ctx context.Context, req *CloseRequest) (*CloseResponse, error)
		Query(ctx context.Context, req *MerchantGetOrderRequest) (*MerchantGetOrderReply, error)
		ParseCallbackResponse(req *AllReq) (*PayCallbackRequest, error)
	}

	payImpl struct {
		baseURL string

		payPublicKey, clientPrivateKey string

		httpCli http.Client
		headers map[string]string
	}
)

var HttpCli = http.Client{
	Transport: &http.Transport{
		MaxIdleConns:        100,
		MaxIdleConnsPerHost: 100,
		IdleConnTimeout:     50 * time.Second,
	},
}

var ErrHttpStatusCode = errors.New("http status code not 200, request fail")
var ErrHttpBodyEmpty = errors.New("response body is empty")

func NewPay(baseURL string, payPublicKey, clientPrivateKey string) IPay {
	return &payImpl{
		baseURL:          baseURL,
		payPublicKey:     payPublicKey,
		clientPrivateKey: clientPrivateKey,
		httpCli:          HttpCli,
		headers:          make(map[string]string),
	}
}

func (p *payImpl) WithHeaders(headers map[string]string) IPay {
	pCopy := p.copy()
	for k, v := range headers {
		pCopy.headers[k] = v
	}
	return pCopy
}

func (p *payImpl) All(ctx context.Context, req *AllReq, m method) (*AllResp, error) {
	response, err := p.post(ctx, p.getPayCenterRequestURL(string(m)), req.Bytes())
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	body, err := io.ReadAll(response.Body)
	if err != nil {
		return nil, err
	}
	if response.StatusCode != http.StatusOK {
		return nil, errors.Join(ErrHttpStatusCode, errors.New(fmt.Sprintf("http status code is %d, body: %s", response.StatusCode, string(body))))
	}

	if len(body) == 0 {
		return nil, ErrHttpBodyEmpty
	}

	var allResp *AllResp
	if err = json.Unmarshal(body, &allResp); err != nil {
		return nil, errors.Join(err, errors.New(fmt.Sprintf("response body is %s", string(body))))
	}
	return allResp, nil
}

func (p *payImpl) Pay(ctx context.Context, req *PayRequest) (*PayResponse, error) {
	params, err := p.getPayCenterRequest(req.Bytes(), p.payPublicKey)
	if err != nil {
		return nil, err
	}
	allResp, err := p.All(ctx, params, MethodPay)
	if err != nil {
		return nil, err
	}
	return allResp.ParsePayResponse(p.clientPrivateKey)
}

func (p *payImpl) Refund(ctx context.Context, req *RefundRequest) (*RefundResponse, error) {
	params, err := p.getPayCenterRequest(req.Bytes(), p.payPublicKey)
	if err != nil {
		return nil, err
	}
	allResp, err := p.All(ctx, params, MethodRefund)
	if err != nil {
		return nil, err
	}
	return allResp.ParseRefundResponse(p.clientPrivateKey)
}

func (p *payImpl) Close(ctx context.Context, req *CloseRequest) (*CloseResponse, error) {
	params, err := p.getPayCenterRequest(req.Bytes(), p.payPublicKey)
	if err != nil {
		return nil, err
	}
	allResp, err := p.All(ctx, params, MethodClose)
	if err != nil {
		return nil, err
	}
	return allResp.ParseCloseResponse(p.clientPrivateKey)
}

func (p *payImpl) Query(ctx context.Context, req *MerchantGetOrderRequest) (*MerchantGetOrderReply, error) {
	params, err := p.getPayCenterRequest(req.Bytes(), p.payPublicKey)
	if err != nil {
		return nil, err
	}
	allResp, err := p.All(ctx, params, MethodQuery)
	if err != nil {
		return nil, err
	}
	return allResp.ParseQueryResponse(p.clientPrivateKey)
}

func (p *payImpl) ParseCallbackResponse(req *AllReq) (*PayCallbackRequest, error) {
	return req.ParseCallbackResponse(p.clientPrivateKey)
}

func (p *payImpl) copy() *payImpl {
	headers := make(map[string]string)
	for k, v := range p.headers {
		headers[k] = v
	}
	return &payImpl{
		baseURL:          p.baseURL,
		payPublicKey:     p.payPublicKey,
		clientPrivateKey: p.clientPrivateKey,
		httpCli:          p.httpCli,
		headers:          headers,
	}
}

func (p *payImpl) post(ctx context.Context, url string, data []byte) (*http.Response, error) {
	reader := bytes.NewReader(data)
	// 设置请求头
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, reader)
	if err != nil {
		return nil, err
	}
	// 没有请求头时候, 默认设置请求头json, utf-8
	req.Header.Set("Content-Type", "application/json; charset=utf-8")
	for k, v := range p.headers {
		req.Header.Set(k, v)
	}
	return p.httpCli.Do(req)
}

// 获取请求地址
func (p *payImpl) getPayCenterRequestURL(api string) string {
	// 去除domain后缀的/
	domainTmp := strings.TrimSuffix(p.baseURL, "/")
	return domainTmp + api
}

// getPayCenterRequest 获取支付中心请求参数
func (p *payImpl) getPayCenterRequest(request []byte, publicKey string) (*AllReq, error) {
	aesKey := NewUUIDString()
	aesEncrypt, err := payserver.NewAesECB(payserver.WithAesECBKey(aesKey))
	if err != nil {
		return nil, err
	}
	parsePublicKey, err := payserver.ParsePublicKey(publicKey)
	if err != nil {
		return nil, err
	}

	rsaEncrypt, err := payserver.NewRSAEncrypt(payserver.WithPublicKey(parsePublicKey))
	if err != nil {
		return nil, err
	}
	enAesKey, err := rsaEncrypt.Encrypt([]byte(aesKey))
	if err != nil {
		return nil, err
	}
	paramsJsonByte := request
	encryptBizParams, err := aesEncrypt.Encrypt(paramsJsonByte)
	if err != nil {
		return nil, err
	}
	requestParams := &AllReq{
		BizParams: string(encryptBizParams),
		AesKey:    string(enAesKey),
	}
	return requestParams, nil
}

// NewUUIDString 生成一个基于UUID的随机字符串
func NewUUIDString() string {
	id := uuid.NewV4().String()
	return strings.ReplaceAll(id, "-", "")
}
